package pl.cezary.butler.walili.server;

import java.rmi.RemoteException;
import pl.cezary.butler.walili.server.db.Database;
import pl.cezary.butler.walili.server.db.utils.StorageBD;
import pl.cezary.butler.walili.server.db.tasks.*;
import pl.cezary.butler.walili.common.IServerFacadeRemote;
import pl.cezary.butler.walili.common.Task;
import pl.cezary.butler.walili.common.Query;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import pl.cezary.butler.walili.common.TaskStatus;
import pl.cezary.butler.walili.server.config.ServerConfiguration;
import pl.cezary.butler.walili.server.db.tasks.GetTasksByStatusTask;
import pl.cezary.butler.walili.server.db.utils.UnitInterruptedException;

/**
 * Klasa implementujaca fasade serwera do poprawnego dzialania wymaga aby w
 * srodowisku byla uruchomiona baza danych
 *
 * @author Cezary Butler
 */
public class DbBasedServerFacade extends AbstractServerFacade implements IServerFacade, IServerFacadeRemote {

    private static DbBasedServerFacade INSTANCE = null;
    private final Database baza = StorageBD.getBD();
    private final TaskDoneRegisteringThread wykonania;
    private final FetchedTasksRegisteringThread pobrania;
    private final RevalidationThread weryfikacja;
    private final Thread dodawanie;
    private static int MAX_PRIORITY = ServerConfiguration.getInstance().getMaxPriority();
    private static int MAX_PRIORITY_INFO_DELTA = 3;

    // <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
    // #[regen=yes,id=DCE.E1D78EED-2FEB-066E-5642-60A8822D0728]
    // </editor-fold> 
    @Override
    public List<Task> processQuery(Query zapytanie) {
        final GetTasksTask zad = new GetTasks4UserTask(zapytanie);
        baza.executeDbTask(zad);
        final List<? extends Task> res = zad.getResults();
        if(log.isLoggable(Level.FINE))
              log.log(Level.FINE, "Query {0} has been processed, result has {1} elements", 
                      new Object[]{zapytanie,res.size()});
        return Task.trim(res);
    }

    private DbBasedServerFacade() {
        if (INSTANCE != null) {
            throw new IllegalStateException("Should be a singleton");
        }
        fetchNewTasks();
        wykonania = new TaskDoneRegisteringThread(this);
        pobrania = new FetchedTasksRegisteringThread(this);
        weryfikacja = new RevalidationThread(this);
        dodawanie = new Thread(new SendingTasksThread(),"New tasks queue");
        startThreads();
    }
    
    public static DbBasedServerFacade createFacade(){
        if(INSTANCE == null){
            INSTANCE = new DbBasedServerFacade();
        }
        return INSTANCE;
    }

    private void fetchNewTasks() {
        try {
            new Thread("Task fetching thread") {

                @Override
                public void run() {
                    //add a way to limit fetched tasks
                    final long startt = System.currentTimeMillis();
                    int counter =0;
                    baza.executeDbTask(new FixMimeTypesTasks());
                    baza.executeDbTask(new FillMimeTypesTask());
                    GetTasksByStatusTask task = new GetTasksByStatusTask(TaskStatus.NOWE, TaskStatus.PRZERWANE, TaskStatus.WZNOWIONE);
                    baza.executeDbTask(task);
                    final List<Task> results = task.getResults();
                    for (Task serverTask : results) {
                        assert (serverTask.getStatus().czyPrzejscieDozwolone(TaskStatus.PRZETWARZANE)): String.format("Task with status %s, cannot be set as %s",serverTask.getStatus(),TaskStatus.PRZETWARZANE);
                        if (serverTask.getPriorytet() < MAX_PRIORITY){
                            kolejkaZadan.offer(ServerTask.getZadanieSerwer(serverTask));
                            ++counter;
                        }
                    }
                    log.log(Level.INFO,"{0} tasks were fetched from db in {1} ms",new Object[]{counter,System.currentTimeMillis()-startt});
                }
            }.start();
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException ex) {
            Logger.getLogger(DbBasedServerFacade.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void startThreads() {
        wykonania.start();
        pobrania.start();
        weryfikacja.start();
        dodawanie.start();
    }

    public static DbBasedServerFacade getInstance() {
        assert (INSTANCE != null);
        return INSTANCE;
    }

    void setInstance(DbBasedServerFacade a) {
        INSTANCE = a;
    }

    @Override
    public ArrayList<Task/*
             * Serwer
             */> getTasks(int n) {
        ArrayList<Task> retval = new ArrayList<Task>(Math.min(n, kolejkaZadan.size()));
        int i = 0;
        while (i < n && !kolejkaZadan.isEmpty()) {
            Task zad = getTask();
            if (zad == null) {
                break;
            } else {
                retval.add(zad);
            }
            ++i;
        }
        retval.trimToSize();
        assert (retval != null);
        return retval;
    }
    
    private BlockingQueue<Task> newTasksBuffer = new ArrayBlockingQueue<Task>(512);
    private int queuedTasks =0 ;

    @Override
    public void sendTask(Task zadanie){
        final int priorytet = zadanie.getPriorytet();
        if (priorytet > MAX_PRIORITY) {
            Level lvl = Level.FINE;
            if (priorytet - MAX_PRIORITY < MAX_PRIORITY_INFO_DELTA) {
                lvl = Level.INFO;
            }
            if (log.isLoggable(lvl)) {
                log.log(lvl, "Task {0} dropped due to to high priority: {1}", new Object[]{zadanie.getCel(), priorytet});
            }
            return;
        }
        if(newTasksBuffer.remainingCapacity()==0)
            log.warning("New task queue will be full any time now");
        if(++queuedTasks%100==0)log.log(Level.INFO,"Queued {0} tasks {1} in queue", new Object[]{queuedTasks,newTasksBuffer.size()});
        newTasksBuffer.offer(zadanie);
    }    
    
    private class SendingTasksThread implements Runnable{

        @Override
        public void run() {
            Task task;
            try {
                while(!UnitInterruptedException.isInterrupted()){
                    
                    while((task= newTasksBuffer.poll(500, TimeUnit.MILLISECONDS))!=null){
                        $sendTask(task);
                    }
                }
            } catch (InterruptedException ex) {
                Logger.getLogger(DbBasedServerFacade.class.getName()).log(Level.SEVERE, null, ex);
            } catch (UnitInterruptedException ex) {
                Logger.getLogger(DbBasedServerFacade.class.getName()).log(Level.SEVERE, null, ex);
            } 
            final BlockingQueue<Task> taskBuffer = newTasksBuffer;
            newTasksBuffer=new ArrayBlockingQueue<Task>(0);//
            log.log(Level.INFO,"Sending tasks was interrupted {0} tasks left in a queue to finish. Finishing",newTasksBuffer.size());
            if(!taskBuffer.isEmpty()){
                try {
                    while((task= taskBuffer.poll(500, TimeUnit.MILLISECONDS))!=null){
                        $sendTask(task);
                    }
                } catch (InterruptedException ex) {
                    Logger.getLogger(DbBasedServerFacade.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }            
    }
    
    private void $sendTask(Task zadanie) {        
        
        if (getTaskStorage().contains(zadanie)) {
            Task zKolejki = getTaskStorage().get(zadanie);
            zKolejki.updatePriorytet(zadanie);
            String pol = zKolejki.getPolozenie();
            if ((pol == null && pol != zadanie.getPolozenie()) || ((pol != null) && !pol.equals(zadanie.getPolozenie()))) {
                baza.executeDbTask(new CreateTaskTask(zadanie));
            }
        } else {
            final Calendar dateOp = Calendar.getInstance();
            dateOp.add(Calendar.DAY_OF_MONTH, -1);
            ServerTask zad = ServerTask.getZadanieSerwer(zadanie);
            if (zad.getDataPrzetwarzania() == null || zad.getDataPrzetwarzania().before(dateOp.getTime())) {
                //FIXME problem gdy pojawia się link zwalidowany podczas poprzedniej sesji, taki nie może trafić tendy do kolejkim
                // ale należałoby zapisać jego położenie
                baza.executeDbTask(new CreateTaskTask(zadanie));
                if (zad.getStatus().czyPrzejscieDozwolone(TaskStatus.PRZETWARZANE)) {
                    kolejkaZadan.offer(zad);
                }
                getTaskStorage().add(zad);
            }
        }

        // dalej nie ma znaczenia co sie stanie
    }

    void close() {        
        wykonania.interrupt();
        pobrania.interrupt();
        weryfikacja.interrupt();
        dodawanie.interrupt();
        try {
            wykonania.join();
            pobrania.join();
            weryfikacja.join();
            dodawanie.join();
        } catch (InterruptedException ie) {
            log.log(Level.INFO, "Przerwano oczekiwanie na zakończenie wątków", ie);
        }
        baza.rozlacz();
    }

    @Override
    public void registerTasksSent(UserId user, Collection<Task> zadanie) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public void retryFailedTasks() throws RemoteException {
        baza.executeDbTask(new RetryFailedTask());
        FetchingTask<List<Task>> task = new GetTasksByStatusTask(TaskStatus.WZNOWIONE);
        baza.executeDbTask(task);
        for (Task serverTask : task.getResults()) {
            assert (serverTask.getStatus().czyPrzejscieDozwolone(TaskStatus.PRZETWARZANE));
            kolejkaZadan.offer(ServerTask.getZadanieSerwer(serverTask));
        }
    }

    @Override
    public void retry(Task zad) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    void prepareForClose() {
        baza.startClosing();
    }

    @Override
    public void fillMimeTypes() throws RemoteException {
        UpdateTask task = new FillMimeTypesTask();
        baza.executeDbTask(task);
    }
        
}
